'use strict';

var adapters = ['local', 'http'];

adapters.forEach(function (adapter) {

  var suiteName = 'test.persisted.js-' + adapter;
  var dbName = testUtils.adapterUrl(adapter, 'testdb');

  tests(suiteName, dbName, adapter);
});

function tests(suiteName, dbName, dbType) {

  describe(suiteName, function () {

    var Promise;

    function setTimeoutPromise(time) {
      return new Promise(function (resolve) {
        setTimeout(function () { resolve(true); }, time);
      });
    }

    function createView(db, viewObj) {
      var storableViewObj = {
        map : viewObj.map.toString()
      };
      if (viewObj.reduce) {
        storableViewObj.reduce = viewObj.reduce.toString();
      }
      return new Promise(function (resolve, reject) {
        db.put({
          _id: '_design/theViewDoc',
          views: {
            'theView' : storableViewObj
          }
        }, function (err) {
          if (err) {
            reject(err);
          } else {
            resolve('theViewDoc/theView');
          }
        });
      });
    }

    beforeEach(function () {
      Promise = testUtils.Promise;
      return new PouchDB(dbName).destroy();
    });
    afterEach(function () {
      return new PouchDB(dbName).destroy();
    });

    it('Test destroyed event on auxiliary db', function () {
      var db = new PouchDB(dbName);
      var rev;
      return db.put({
        _id: '_design/name',
        views: {
          name: {
            map: function (doc) {
              emit(doc.name);
            }.toString()
          }
        }
      }).then(function (res) {
        rev = res.rev;
        return db.bulkDocs([
          {_id: 'foo', name: 'foo', title: 'yo'},
          {_id: 'baz', name: 'bar', title: 'hey'},
          {_id: 'bar', name: 'baz', title: 'wuzzup'}
        ]);
      }).then(function () {
        return db.query('name');
      }).then(function () {
        return db.remove('_design/name', rev);
      }).then(function () {
        return db.viewCleanup();
      }).then(function () {
        return db.put({
          _id: '_design/title',
          views: {
            title: {
              map: function (doc) {
                emit(doc.title);
              }.toString()
            }
          }
        });
      }).then(function (res) {
        rev = res.rev;
      }).then(function () {
        return db.query('title');
      }).then(function () {
        return db.remove('_design/title', rev);
      }).then(function () {
        return db.viewCleanup();
      }).then(function () {
        var views = ['name', 'title'];
        return testUtils.Promise.all(views.map(function (view) {
          return db.query(view).then(function () {
            throw new Error('expected an error');
          }, function (err) {
            should.exist(err);
          });
        }));
      }).then(function () {
        return db.put({
          _id: '_design/name',
          views: {
            name: {
              map: function (doc) {
                emit(doc.name);
              }.toString()
            }
          }
        }).then(function (res) {
          rev = res.rev;
          return db.query('name');
        }).then(function (res) {
          res.rows.map(function (row) {
            return [row.id, row.key];
          }).should.deep.equal([
              ['baz', 'bar'],
              ['bar', 'baz'],
              ['foo', 'foo']
            ]);
        });
      });
    });

    it('Returns ok for viewCleanup on empty db', function () {
      var db = new PouchDB(dbName);
      return db.viewCleanup().then(function (res) {
        res.ok.should.equal(true);
      });
    });

    it('Returns ok for viewCleanup on empty db, callback style', function () {
      var db = new PouchDB(dbName);
      return new Promise(function (resolve, reject) {
        db.viewCleanup(function (err, res) {
          if (err) {
            return reject(err);
          }
          resolve(res);
        });
      }).then(function (res) {
        res.ok.should.equal(true);
      });
    });

    it('Returns ok for viewCleanup after modifying view', function () {
      var db = new PouchDB(dbName);
      var ddoc = {
        _id: '_design/myview',
        views: {
          myview: {
            map: function (doc) {
              emit(doc.firstName);
            }.toString()
          }
        }
      };
      var doc = {
        _id: 'foo',
        firstName: 'Foobar',
        lastName: 'Bazman'
      };
      return db.bulkDocs({docs: [ddoc, doc]}).then(function (info) {
        ddoc._rev = info[0].rev;
        return db.query('myview');
      }).then(function (res) {
        res.rows.should.deep.equal([
          {id: 'foo', key: 'Foobar', value: null}
        ]);
        ddoc.views.myview.map = function (doc) {
          emit(doc.lastName);
        }.toString();
        return db.put(ddoc);
      }).then(function () {
        return db.query('myview');
      }).then(function (res) {
        res.rows.should.deep.equal([
          {id: 'foo', key: 'Bazman', value: null}
        ]);
        return db.viewCleanup();
      });
    });

    it('Return ok for viewCleanup after modding view, old format', function () {
      var db = new PouchDB(dbName);
      var ddoc = {
        _id: '_design/myddoc',
        views: {
          myview: {
            map: function (doc) {
              emit(doc.firstName);
            }.toString()
          }
        }
      };
      var doc = {
        _id: 'foo',
        firstName: 'Foobar',
        lastName: 'Bazman'
      };
      return db.bulkDocs({docs: [ddoc, doc]}).then(function (info) {
        ddoc._rev = info[0].rev;
        return db.query('myddoc/myview');
      }).then(function (res) {
        res.rows.should.deep.equal([
          {id: 'foo', key: 'Foobar', value: null}
        ]);
        ddoc.views.myview.map = function (doc) {
          emit(doc.lastName);
        }.toString();
        return db.put(ddoc);
      }).then(function () {
        return db.query('myddoc/myview');
      }).then(function (res) {
        res.rows.should.deep.equal([
          {id: 'foo', key: 'Bazman', value: null}
        ]);
        return db.viewCleanup();
      });
    });

    it("Query non existing view throws error", function () {
      var db = new PouchDB(dbName);
      var doc = {
        _id: '_design/barbar',
        views: {
          scores: {
            map: 'function(doc) { if (doc.score) { emit(null, doc.score); } }'
          }
        }
      };
      return db.post(doc).then(function () {
        return db.query('barbar/dontExist', {key: 'bar'}).should.be.rejected;
      });
    });

    it("Query non-string view throws error", function () {
      var db = new PouchDB(dbName);
      var doc = {
        _id: '_design/barbar',
        views: {
          scores: {
            map: 1
          }
        }
      };
      return db.post(doc).then(function () {
        return db.query('barbar/scores', {key: 'bar'}).should.be.rejected;
      });
    });

    it('many simultaneous persisted views', function () {
      this.timeout(120000);
      var db = new PouchDB(dbName);

      var views = [];
      var doc = {_id: 'foo'};
      for (var i = 0; i < 20; i++) {
        views.push('foo_' + i);
        doc['foo_' + i] = 'bar_' + i;
      }

      return db.put(doc).then(function () {
        return Promise.all(views.map(function (_, i) {
          var fun = "function (doc) { emit(doc.foo_" + i + ");}";

          var ddocId = 'theViewDoc_' + i;
          var ddoc = {
            _id: '_design/' + ddocId,
            views: {
              theView : {map: fun}
            }
          };

          return db.put(ddoc).then(function (res) {
            ddoc._rev = res.rev;
            return db.query(ddocId + '/theView');
          }).then(function (res) {
            res.rows.should.have.length(1);
            res.rows[0].key.should.equal('bar_' + i);
            res.rows[0].id.should.equal('foo');
            return db.remove(ddoc);
          }).then(function () {
            return db.viewCleanup();
          }).then(function () {
            return db.query(ddocId + '/theView').then(function () {
              throw new Error('view should have been deleted');
            }, function (err) {
              should.exist(err);
            });
          });
        }));
      });
    });

    it('should error with a callback', function (done) {
      var db = new PouchDB(dbName);
      db.query('fake/thing', function (err) {
        should.exist(err);
        done();
      });
    });

    it('should query correctly when stale', function () {
      var db = new PouchDB(dbName);
      return createView(db, {
        map : function (doc) {
          emit(doc.name);
        }
      }).then(function (queryFun) {
        return db.bulkDocs({docs : [
          {name : 'bar', _id : '1'},
          {name : 'foo', _id : '2'}
        ]}).then(function () {
          return db.query(queryFun, {stale : 'ok'});
        }).then(function (res) {
          res.total_rows.should.be.within(0, 2);
          res.offset.should.equal(0);
          res.rows.length.should.be.within(0, 2);
          return db.query(queryFun, {stale : 'update_after'});
        }).then(function (res) {
          res.total_rows.should.be.within(0, 2);
          res.rows.length.should.be.within(0, 2);
          return setTimeoutPromise(5);
        }).then(function () {
          return db.query(queryFun, {stale : 'ok'});
        }).then(function (res) {
          res.total_rows.should.equal(2);
          res.rows.length.should.equal(2);
          return db.get('2');
        }).then(function (doc2) {
          return db.remove(doc2);
        }).then(function () {
          return db.query(queryFun, {stale : 'ok', include_docs : true});
        }).then(function (res) {
          res.total_rows.should.be.within(1, 2);
          res.rows.length.should.be.within(1, 2);
          if (res.rows.length === 2) {
            res.rows[1].key.should.equal('foo');
            should.not.exist(res.rows[1].doc,
                             'should not throw if doc removed');
          }
          return db.query(queryFun);
        }).then(function (res) {
          res.total_rows.should.equal(1, 'equals1-1');
          res.rows.length.should.equal(1, 'equals1-2');
          return db.get('1');
        }).then(function (doc1) {
          doc1.name = 'baz';
          return db.post(doc1);
        }).then(function () {
          return db.query(queryFun, {stale : 'update_after'});
        }).then(function (res) {
          res.rows.length.should.equal(1);
          ['baz', 'bar'].indexOf(res.rows[0].key).should.be
            .above(-1, 'key might be stale, thats ok');
          return setTimeoutPromise(1000);
        }).then(function () {
          return db.query(queryFun, {stale : 'ok'});
        }).then(function (res) {
          res.rows.length.should.equal(1);
          res.rows[0].key.should.equal('baz');
        });
      });
    });

    it('should query correctly with stale update_after', function () {
      var pouch = new PouchDB(dbName);

      return createView(pouch, {map: function (doc) {
        emit(doc.foo);
      }}).then(function (queryFun) {
        var docs = [];

        for (var i = 0; i < 10; i++) {
          docs.push({foo: 'bar'});
        }

        return pouch.bulkDocs(docs).then(function () {
          return pouch.query(queryFun, {stale: 'update_after'});
        }).then(function (res) {
          res.rows.should.have.length(0, 'query() returned immediately');
          return setTimeoutPromise(1000);
        }).then(function () {
          return pouch.query(queryFun, {stale: 'ok'});
        }).then(function (res) {
          res.rows.should.have.length(10, 'index was built in background');
        });
      });
    });

    it('should delete duplicate indexes', function () {
      var docs = [];
      for (var i = 0; i < 10; i++) {
        docs.push(
          {
            _id : '_design/view' + i,
            views : {
              view : {
                map : "function(doc){emit('foo');}"
              }
            }
          }
        );
      }
      var db = new PouchDB(dbName);
      return db.bulkDocs({docs : docs}).then(function (responses) {
        var tasks = [];
        for (var i = 0; i < docs.length; i++) {
          /* jshint loopfunc:true */
          docs[i]._rev = responses[i].rev;
          tasks.push(db.query('view' + i + '/view'));
        }
        return Promise.all(tasks);
      }).then(function () {
        docs.forEach(function (doc) {
          doc._deleted = true;
        });
        return db.bulkDocs({docs : docs});
      }).then(function () {
        return db.viewCleanup();
      });
    });

    if (dbType === 'local' &&
        // can't test this in Node due to the vm
        (typeof process === 'undefined' || process.browser)) {
      it('issue 4967 map() called twice', function () {
        var db = new PouchDB(dbName);
        var globalObj = (typeof process !== 'undefined' && !process.browser) ?
          global : window;
        globalObj.__mapreduce_called = {};
        var docs = Array.apply(null, Array(5)).map(function (_, i) {
          return {
            _id: 'doc_' + i,
            data: Math.random().toString(36).substr(2)
          };
        }).concat({
          _id: '_design/test',
          views: {
            test: {
              map: (function (doc) {
                /* global __mapreduce_called */
                __mapreduce_called[doc._id] = __mapreduce_called[doc._id] || 0;
                __mapreduce_called[doc._id]++;
                emit(doc.data, 1);
              }).toString()
            }
          }
        });
        return db.bulkDocs(docs).then(function () {
          return Promise.all([
            db.query('test', {}),
            db.query('test', {})
          ]);
        }).then(function () {
          globalObj.__mapreduce_called.should.deep.equal({
            doc_0 : 1,
            doc_1 : 1,
            doc_2 : 1,
            doc_3 : 1,
            doc_4 : 1
          });
          delete globalObj.__mapreduce_called;
        });
      });
    }

    it('test docs with reserved IDs', function () {
      var db = new PouchDB(dbName);

      var docs = [
        {_id: 'constructor'},
        {_id: 'isPrototypeOf'},
        {_id: 'hasOwnProperty'},
        {
          _id : '_design/view',
          views : {
            view : {
              map : "function(doc){emit(doc._id);}"
            }
          }
        }
      ];
      return db.bulkDocs(docs).then(function () {
        return db.query('view/view', {include_docs: true});
      }).then(function (res) {
        var rows = res.rows.map(function (row) {
          return {
            id: row.id,
            key: row.key,
            docId: row.doc._id
          };
        });
        assert.deepEqual(rows, [
          { "id": "constructor",
            "key": "constructor",
            "docId": "constructor"
          },
          {
            "id": "hasOwnProperty",
            "key": "hasOwnProperty",
            "docId": "hasOwnProperty"
          },
          {
            "id": "isPrototypeOf",
            "key": "isPrototypeOf",
            "docId": "isPrototypeOf"
          }
        ]);
        return db.viewCleanup();
      }).then(function () {
        return db.get('_design/view');
      }).then(function (doc) {
        return db.remove(doc);
      }).then(function () {
        return db.viewCleanup();
      });
    });

    it('should handle user errors in design doc names', function () {
      var db = new PouchDB(dbName);
      return db.put({
        _id : '_design/theViewDoc'
      }).then(function () {
        return db.query('foo/bar');
      }).then(function (res) {
        should.not.exist(res);
      }).catch(function (err) {
        err.status.should.equal(404);
        return db.put(
          {_id : '_design/void', views : {1 : null}}
        ).then(function () {
          return db.query('void/1');
        }).then(function (res) {
          should.not.exist(res);
        }).catch(function (err) {
          err.status.should.be.a('number');
          // this might throw due to erroneous ddoc, but that's ok
          return db.viewCleanup().catch(function (err) {
            err.status.should.equal(500);
          });
        });
      });
    });

    it('should allow the user to create many design docs', function () {
      function getKey(row) {
        return row.key;
      }
      var db = new PouchDB(dbName);
      return db.put({
        _id : '_design/foo',
        views : {
          byId : { map : function (doc) { emit(doc._id); }.toString()},
          byField : { map : function (doc) { emit(doc.field); }.toString()}
        }
      }).then(function () {
        return db.put({_id : 'myDoc', field : 'myField'});
      }).then(function () {
        return db.query('foo/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
        return db.put({
          _id : '_design/bar',
          views : {
            byId : {map : function (doc) { emit(doc._id); }.toString()}
          }
        });
      }).then(function () {
        return db.query('bar/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
      }).then(function () {
        return db.viewCleanup();
      }).then(function () {
        return db.query('foo/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
        return db.query('foo/byField');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myField']);
        return db.query('bar/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
        return db.get('_design/bar');
      }).then(function (barDoc) {
        return db.remove(barDoc);
      }).then(function () {
        return db.get('_design/foo');
      }).then(function (fooDoc) {
        delete fooDoc.views.byField;
        return db.put(fooDoc);
      }).then(function () {
        return db.query('foo/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
        return db.viewCleanup();
      }).then(function () {
        return db.query('foo/byId');
      }).then(function (res) {
        res.rows.map(getKey).should.deep.equal(['myDoc']);
        return db.query('foo/byField').then(function (res) {
          should.not.exist(res);
        }).catch(function (err) {
          err.status.should.equal(404);
          return db.query('bar/byId').then(function (res) {
            should.not.exist(res);
          }).catch(function (err) {
            err.status.should.equal(404);
            return db.get('_design/foo').then(function (fooDoc) {
              return db.remove(fooDoc).then(function () {
                return db.viewCleanup();
              });
            });
          });
        });
      });
    });

    it('should allow view names without slashes', function () {
      var ddocRev;
      var db = new PouchDB(dbName);
      return db.put({
        _id : '_design/foo',
        views : {
          foo : { map : function (doc) { emit(doc._id); }.toString()}
        }
      }).then(function (info) {
        ddocRev = info.rev;
        return db.bulkDocs({docs : [{_id : 'baz'}, {_id : 'bar'}]});
      }).then(function () {
        return db.query('foo');
      }).then(function (res) {
        res.rows[0].key.should.equal('bar');
        res.rows[1].key.should.equal('baz');
        return db.remove({_id : '_design/foo', _rev : ddocRev});
      });
    });

    it('test 304s in Safari (issue 69)', function () {
      var db = new PouchDB(dbName);
      return createView(db, {
        map : function (doc) {
          emit(doc.name);
        }
      }).then(function (queryFun) {
        return db.bulkDocs({docs : [
          {name : 'foo'}
        ]}).then(function () {
          return db.query(queryFun, {keys : ['foo']});
        }).then(function (res) {
          res.rows.should.have.length(1);
          return db.query(queryFun, {keys : ['foo']});
        }).then(function (res) {
          res.rows.should.have.length(1);
          return db.query(queryFun, {keys : ['foo']});
        }).then(function (res) {
          res.rows.should.have.length(1);
        });
      });
    });

    var isNode = typeof window === 'undefined';
    if (dbType === 'local' && isNode) {
      it('#239 test memdown db', function () {
        var destroyedDBs = [];
        PouchDB.on('destroyed', function (db) {
          destroyedDBs.push(db);
        });

        // make sure prefixed DBs are tied to regular DBs
        var db = new PouchDB(dbName, {db: require('memdown')});
        return testUtils.fin(createView(db, {
          map: function (doc) {
            emit(doc.name);
          }
        }).then(function (queryFun) {
          return db.post({name: 'foo'}).then(function () {
            return db.query(queryFun);
          }).then(function (res) {
            res.rows.should.have.length(1);
            res.rows[0].key.should.equal('foo');
            var ddocId = '_design/' + queryFun.split('/')[0];
            return db.get(ddocId);
          }).then(function (ddoc) {
            return db.remove(ddoc);
          }).then(function () {
            return db.viewCleanup();
          });
        }), function () {
          return db.destroy().then(function () {
            var chain = Promise.resolve();
            // for each of the supposedly destroyed DBs,
            // check that there isn't a normal DB hanging around
            destroyedDBs.forEach(function (dbName) {
              chain = chain.then(function () {
                var db = new PouchDB(dbName);
                var promise = db.info().then(function (info) {
                  info.update_seq.should.equal(0);
                });
                return testUtils.fin(promise, function () {
                  return db.destroy();
                });
              });
            });
            return chain;
          }).then(function () {
            PouchDB.removeAllListeners('destroyed');
          });
        });
      });

      it('#239 test prefixed db', function () {
        var destroyedDBs = [];
        PouchDB.on('destroyed', function (db) {
          destroyedDBs.push(db);
        });

        // make sure prefixed DBs are tied to regular DBs
        require('mkdirp').sync('./myprefix_./tmp/'); // TODO: bit hacky
        var db = new PouchDB(dbName, {prefix: './myprefix_'});
        return testUtils.fin(createView(db, {
          map: function (doc) {
            emit(doc.name);
          }
        }).then(function (queryFun) {
          return db.post({name: 'foo'}).then(function () {
            return db.query(queryFun);
          }).then(function (res) {
            res.rows.should.have.length(1);
            res.rows[0].key.should.equal('foo');
            var ddocId = '_design/' + queryFun.split('/')[0];
            return db.get(ddocId);
          }).then(function (ddoc) {
            return db.remove(ddoc);
          }).then(function () {
            return db.viewCleanup();
          });
        }), function () {
          return db.destroy().then(function () {
            var chain = Promise.resolve();
            // for each of the supposedly destroyed DBs,
            // check that there isn't a normal DB hanging around
            destroyedDBs.forEach(function (dbName) {
              chain = chain.then(function () {
                var db = new PouchDB(dbName);
                var promise = db.info().then(function (info) {
                  info.update_seq.should.equal(0);
                });
                return testUtils.fin(promise, function () {
                  return db.destroy();
                });
              });
            });
            return chain;
          }).then(function () {
            PouchDB.removeAllListeners('destroyed');
          });
        });
      });
    }

  });
}
